{T}

Node.js 核心 API 速通(五)

这节来过一下剩下的 Node.js 的内置模块:

vm

vm 模块可以创建一个 JS 执行环境。

比如写单测的时候,你会发现 test、describe 这些都是全局变量,但写其他 JS 代码的时候就没有这些。

很明显,test 脚本跑在一个单独的环境里。

那如何创建一个单独的 JS 运行环境呢?

就是用 vm 模块。

创建 vm.mjs

javascript
import vm from 'node:vm';

const context = {
    console,
    guang: 111,
    dong: 222
}

vm.createContext(context);

vm.runInContext('console.log(guang + dong)', context);

用 vm.createContext 创建了一个上下文。

有 console、guang、dong 这三个全局变量。

然后用 runInContext 来跑了一段 JS 代码,传入 context。

跑一下:

bash
node vm.mjs

其实你每天跑的单测,实现原理就是这个。

自定义了 context,所以才可以直接用 test、describe 等全局 api。

repl

repl 是 Read-Eval-Print-Loop,也就是这个东西:

可以交互式的来执行一些脚本。

如果你也想创建这种 repl,直接用 repl 模块就行。

创建 repl.mjs

javascript
import repl from 'node:repl';

const r = repl.start({ prompt: '> ', eval: myEval});

function myEval(cmd, context, filename, callback) {
    console.log('你输入的命令:' + cmd);
    callback();
}

创建一个 repl,指定 eval 的处理函数。

cmd 是用户输入的内容

context 是 node 的所有 api,比如 context.fs

最后调用 callback 才会结束处理,所以可以异步处理,处理完调用 callback

跑一下:

bash
node ./repl.mjs

引入 cfonts 来做下艺术字:

css
npm install --save cfonts

改下代码:

javascript
import repl from 'node:repl';
import cfonts from 'cfonts';

const r = repl.start({ prompt: '> ', eval: myEval});

function myEval(cmd, context, filename, callback) {
    cfonts.say(cmd, {
        font: '3D',
        colors: ['yellow', 'cyan']
    });
    callback();
}

跑一下:

dns

dns 模块,顾名思义就是用来查询域名对应的 IP 的。

试一下:

创建 dns.mjs

javascript
import dns from 'node:dns/promises';

async function main() {
    const res = await dns.resolve('juejin.com');
    console.log(res);
}

main();

跑一下:

string_decoder

这个模块是用来从 Buffer 根据编码解析成字符串的。

创建 string_decoder.mjs

javascript
import { StringDecoder } from 'node:string_decoder';
import { Buffer } from 'node:buffer';

const decoder = new StringDecoder('utf8');

const buf = Buffer.from('神说要有光', 'utf-8');
console.log(decoder.write(buf));

学过 Buffer.from 是用来创建 buffer 的,然后用 StringDecoder 就可以把 Buffer 解析为字符串。

跑一下:

worker_threads

学过 child_process 是用来创建子进程的,其实 node 也支持创建工作线程。

就是 worker_threads 模块。

比如很大计算量的任务,放在主线程会很耗时,完全可以把它拆到工作线程中去,异步处理。

创建 worker_threads_main.mjs

javascript
import { Worker, MessageChannel } from 'node:worker_threads';

const { port1, port2 } = new MessageChannel();

const worker = new Worker('./worker_threads_worker.mjs');
worker.postMessage(
    { value: 10*10000*10000, channel: port2 },
    [port2]
);

port1.on('message', (value) => {
    console.log('res', value);
})

创建主线程,用 new Worker 创建工作线程。

MessageChannel 是创建通信的通道,它有两个端口 port1 和 port2。

你在 port1 里 postMessage,在 port2 里就可以用 message 事件接收到消息,反过来也是。

很容易理解,就像一个管子的两个口:

其实和浏览器里的工作线程的 api 是差不多的:

然后创建工作线程 worker_threads_worker.mjs

javascript
import { parentPort } from 'node:worker_threads';

function calc(num) {
    let total = 0;
    for(let i = 0; i< num; i++) {
        total += i;
    }
    return total
}

parentPort.on('message', (message) => {
    const res = calc(message.value);

    message.channel.postMessage(res);
});

工作线程通过 parentPort 和父线程通信。

跑一下:

bash
node ./worker_threads_main.mjs

可以看到,工作线程计算出了结果,并且异步的返回了。

readline

readline 字面意思来看是一行一行的读取,这确实是它的功能之一。

创建 readline.mjs

javascript
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

const rl = createInterface({
  input: createReadStream('./repl.mjs')
});

rl.on('line', (line) => {
  console.log(`Line from file: ${line}`);
});

按照行来读取文件可读流里的内容。

跑一下:

javascript
node readline.mjs

当然,它的 input 也可以是标准输入 stdin,这样就变成了类似 repl 的效果:

创建 readline2.mjs

javascript

import { createInterface } from 'node:readline';
import { exit, stdin, stdout } from 'node:process';

const rl = createInterface({
  input: stdin,
  output: stdout,
  prompt: 'guang> ',
});

rl.prompt();

rl.on('line', (line) => {
  switch (line.trim()) {
    case 'hello':
      console.log('world!');
      break;
    default:
      console.log(`你说啥?我听到的是 '${line.trim()}'`);
      break;
  }
  rl.prompt();
}).on('close', () => {
  console.log('bye!');
  exit(0);
});

跑一下:

同样是按行读取,只不过现在读取的是 stdin,也就是用户输入。

当用户按回车的时候来处理。

而且还支持输出 prompt,这样确实和 repl 效果一样了。

此外,readline 还可以支持键盘输入。

之前写过一个例子:

创建 readline3.mjs

javascript
import readline from 'node:readline';

readline.emitKeypressEvents(process.stdin);

process.stdin.setRawMode(true);

process.stdin.on('keypress', (str, key) => {

    console.log(str, key)

    if(key.sequence === '\u0003') {
        process.exit();
    }
});

readline 模块的 emitKeypressEvents 可以让输入流处理键盘事件。

然后 stdin.setRawMode(true) 是禁用掉内置的一些键盘事件处理,比如 ctrl + c 退出进程

监听 keypress 事件,打印下。

自己判断输入的是 ctrl + c 的时候,退出。

这就是 readline 模块的三个功能:按行读取文件、按行读取用户输入(类似 repl)、处理键盘事件

querystring

这个模块也很简单,就是用于 query string 的解析和格式化的。

创建 query_string.mjs

javascript
import queryString from 'node:querystring';

const res = queryString.parse('a=1&b=2&c=xxx');

console.log(res);

const res2 = queryString.stringify({ aaa: '111', bbb: ['222', '33'], ccc: '444' });

console.log(res2);

跑一下:

v8

知道通过 os 模块可以拿到系统的 cpu、内存、网卡等信息。

那如果想知道 Node.js 的 v8 的内存等信息呢?

这种就要用 v8 模块了。

它提供了查询内存状况、设置运行参数等的 api。

创建 v8.mjs

javascript
import v8 from 'node:v8';

console.log(v8.getHeapSpaceStatistics());

console.log(v8.getHeapStatistics());

getHeapSpaceStatistics 是拿到堆内存的每一部分的统计信息。

getHeapStatistics 是拿到堆内存整体的统计信息。

跑一下:

bash
node v8.mjs

可以看到,v8 的堆内存划分了这些部分,并且给出了每一部分的 size 等信息。

而下面是整个堆内存的统计信息:

你还可以运行时给 v8 设置一些 flags,这样:

全部的 v8 flags 可以看这里:

gist.github.com/andrewiggin…

代码上传了文档仓库

总结

这节过了一下剩下的几个模块:

  • vm:创建一个 JS 运行环境,在这个环境里跑 JS 代码,比如 jest 等指定上下文就是通过这个
  • repl:自己创建 repl 的交互的时候,用这个模块
  • dns:查询 dns 也就是域名到 ip
  • string_decoder:用于解码 Buffer 为字符串
  • worker_threads:创建工作线程来提升性能
  • readline:按行读取输入流,可以读取文件、读取用户输入(类似 repl)、还可以处理键盘事件
  • querystring:用来解析和生成 querystring 的
  • v8:拿到 v8 的内存等统计信息,还可以设置 v8 flags

至此,就把 Node.js 的所有核心 API 过了一遍。